from math import sqrt

import numpy as np
import pandas as pd
from scipy.sparse.linalg import svds
from sklearn import model_selection as cv
from sklearn.metrics import mean_squared_error
from sklearn.metrics.pairwise import pairwise_distances

# 推荐算法

header = ['user_id', 'item_id', 'rating', 'timestamp']

df = pd.read_csv('../ml-100k/u.data', sep='\t', names=header)

# 查看用户和电影的数量
n_users = df.user_id.unique().shape[0]
n_items = df.item_id.unique().shape[0]

print('Number of users = ' + str(n_users) + ' | Number of movies = ' + str(n_items))

# 使用scikit-learn库将数据集分割成测试集和训练集，
# 调用Cross_validation.train_test_split根据测试样本的比例(test_size)将数据混洗并分割成两个数据集。
train_data, test_data = cv.train_test_split(df, test_size=0.25)

# 创建用户产品矩阵，针对测试数据和训练数据，创建两个矩阵
train_data_matrix = np.zeros((n_users, n_items))

for line in train_data.itertuples():
    train_data_matrix[line[1] - 1, line[2] - 1] = line[3]

test_data_matrix = np.zeros((n_users, n_items))

for line in test_data.itertuples():
    test_data_matrix[line[1] - 1, line[2] - 1] = line[3]

# 使用sklearn的pairwise_distances函数来计算余弦相似性。
user_similarity = pairwise_distances(train_data_matrix, metric="cosine")
item_similarity = pairwise_distances(train_data_matrix.T, metric="cosine")

# 可以将用户k和用户a之间的相似性看成权重，乘以相似用户a(校正的平均评分用户)的评分，
# 这里需要规范化该值，使得打分位于1到5之间，最后对尝试预测的用户的平均评分求和。
# 基于产品的CF应用下面的公司进行预测，此时无需纠正用户的平均打分
def predict(rating, similarity, type='user'):
    if type == 'user':
        mean_user_rating = rating.mean(axis=1)
        rating_diff = (rating - mean_user_rating[:, np.newaxis])
        pred = mean_user_rating[:, np.newaxis] + similarity.dot(rating_diff) / np.array(
            [np.abs(similarity).sum(axis=1)]).T
    elif type == 'item':
        pred = rating.dot(similarity) / np.array([np.abs(similarity).sum(axis=1)])
    return pred


# 最后对尝试预测的用户的平均评分求和
item_prediction = predict(train_data_matrix, item_similarity, type='item')
user_prediction = predict(train_data_matrix, user_similarity, type='user')


# 评估
# 这里采用均方根误差(RMSE)来度量预测评分的准确性
# 可以使用sklearn的mean_square_error(MSE)函数，其中RMSE仅仅是MSE的平方根。
def rmse(prediction, ground_truth):
    prediction = prediction[ground_truth.nonzero()].flatten()
    ground_truth = ground_truth[ground_truth.nonzero()].flatten()
    return sqrt(mean_squared_error(prediction, ground_truth))


print('User based CF RMSE: ' + str(rmse(user_prediction, test_data_matrix)))
print('Item based CF RMSe: ' + str(rmse(item_prediction, test_data_matrix)))

# 基于模型的协同过滤
# 基于模型的协同过滤是基于矩阵分解(MF)的，矩阵分解广泛应用于推荐系统中，
# 它比基于内存的CF有更好的扩展性和稀疏性。MF的目标是从已知的评分中学习用户的潜在喜好和产品的潜在属性，
# 随后通过用户和产品的潜在特征的点积来预测未知的评分。

sparsity = round(1.0 - len(df) / float(n_users * n_items), 3)

print('The sparsity level of MovieLen100K is ' + str(sparsity * 100) + '%')

# 阵X可以被分解成U，S和V。U矩阵表示对应于隐藏特性空间中的用户的特性矩阵，而V矩阵表示对应于隐藏特性空间中的产品的特性矩阵。
u, s, vt = svds(train_data_matrix, k=20)
s_diag_matrix = np.diag(s)
x_pred = np.dot(np.dot(u, s_diag_matrix), vt)

print('User-based CF MSE: ' + str(rmse(x_pred, test_data_matrix)))
